package shell.parser;

import java.util.ArrayList;
import java.util.List;

import kernel.exception.OSException;
import kernel.process.AbstractProcessHeader;
/**
 * Trida urcena pro rozdeleni radku na jednotlive jeho komponenty.
 * @author Veronika Vencurikova
 *
 */
public class GrammarParser {
	
	/** Konstanta oznacujici parsovani prikazu.*/
	public static final byte PARSER_COMMAND = 0;
	/** Konstanta oznacujici parsovani obecneho parametru. */
	public static final byte PARSER_PARAM = 1;
	/** Konstanta oznacujici parsovani vstupu. */
	public static final byte PARSER_INPUT = 2;
	/** Konstanta oznacujici parsovani vystupu. */
	public static final byte PARSER_OUTPUT = 3;
	/** Konstanta oznacujici parsovani vystupu s pripojenim na konec souboru. */
	public static final byte PARSER_APPEND = 4;
	
	private long shellID;
	private String userID;
	
	private List<String> strings;
	private List<AbstractProcessHeader> fcommands;
	private AbstractProcessHeader lastCommand;
	private byte parsing = PARSER_COMMAND;
	
	/**
	 * Konstruktor.
	 * @param shellID Identifikator shellu.
	 * @param userID  Identifikator uzivatele.
	 */
	public GrammarParser(long shellID, String userID) {
		
		this.shellID = shellID;
		this.userID = userID;
		this.strings = new ArrayList<String>();
		
	}
	
	/**
	 * Setter pro userID.
	 * @param user Jmeno noveho uzivatele.
	 * @return Instance parseru.
	 */
	public GrammarParser setUserID(String user){
		this.userID = user;
		return this;
	}
	
	/**
	 * Urcuje zda muze byt znak obsazen v prikazu.
	 * @param ch Znak.
	 * @return Zda je znak v prikazu povolen.
	 */
	public static boolean isCommandChar(char ch) {
		
		return (Character.isLetterOrDigit(ch) || ch == '_' || ch == '+' || ch == '-' || ch == '.');
		
	}
	
	/**
	 * Urcuje zda muze byt znak obsazen v parametru.
	 * @param ch Znak.
	 * @return Zda je znak v parametru povolen.
	 */
	public static boolean isParameterChar(char ch) {
		
		return (ch > 32 && !Character.isWhitespace(ch));
		
	}
	
	/**
	 * Urcuje zda se jedna o oddelovaci znak mezi parametry.
	 * @param ch Znak.
	 * @return Zda je znak oddelovacem.
	 */
	public static boolean isParameterSeparatorChar(char ch) {
		
		return (Character.isWhitespace(ch) || ch == '"' || ch == '\'' || ch == '<' || ch == '>' || ch == '|');
		
	}
	
	/**
	 * Presunuti prvniho znaku vstupniho StringBuilderu do vystupniho StringBuilderu. 
	 * @param from Vstupni.
	 * @param to Vystupni.
	 */
	private void moveChar(StringBuilder from, StringBuilder to) {
		
		if (from.length() > 0) {
			to.append(from.charAt(0));
			from.deleteCharAt(0);
		}
		
	}
	
	/**
	 * Nastaveni spravneho parametru prikazu.
	 * @param param Parametr.
	 */
	private void setProperParam(String param) {
		
		strings.add(param);
		
		switch (parsing) {
		case PARSER_PARAM:
			lastCommand.getParams().add(param);
			break;
		case PARSER_INPUT:
			lastCommand.setInput(param);
			break;
		case PARSER_OUTPUT:
			lastCommand.setOutput(param);
			lastCommand.setAppend(false);
			break;
		case PARSER_APPEND:
			lastCommand.setOutput(param);
			lastCommand.setAppend(true);
			break;

		default:
			break;
		}
		
		parsing = PARSER_PARAM;

	}
	
	/**
	 * Nacteni parametru v uvozovkach.
	 * @param command Nacitany radek.
	 * @param quote Ukoncovaci uvozovka.
	 * @return Pocet nactenych znaku.
	 * @throws OSException Vyjimka, pokud nejsou uvozovky ukonceny.
	 */
	private int readQuoted(StringBuilder command, char quote) throws OSException {

		boolean ended = false;
		int length = 0;

		if (command.length() > 0) {
			for (int i = 1; i < command.length(); i++) {
				if (command.charAt(i) == quote) {
					length = i - 1;
					ended = true;
					break;
				}
			}
		}
		
		if (!ended) {
			strings.add(command.substring(1));
			throw new OSException("Quote not ended.");
		}
		
		return ++length;
	
	}
	
	/**
	 * Nacteni obecneho parametru.
	 * @param command Nacitana radka.
	 * @return Zbytek nacitane radky.
	 * @throws OSException Vyjimka v pripade nacteni nepovoleneho znaku.
	 */
	private StringBuilder readParameter(StringBuilder command) throws OSException {

		StringBuilder tmp = new StringBuilder();
		int length;

		command = readSpace(command);
		if (command.length() == 0) {
			return command;
		}

		while (command.length() > 0) {
			if (command.charAt(0) == '"') {
				if (tmp.length() > 0) {
					setProperParam(tmp.toString());
					tmp.delete(0, tmp.length());
					//throw new OSException("Quote not allowed here.");
				}
				length = readQuoted(command, '"');
				setProperParam(command.substring(1, length));
				command.delete(0, length + 1);
			} else if (command.charAt(0) == '\'') {
				if (tmp.length() > 0) {
					setProperParam(tmp.toString());
					tmp.delete(0, tmp.length());
					//throw new OSException("Quote not allowed here.");
				}
				length = readQuoted(command, '\'');
				setProperParam(command.substring(1, length));
				command.delete(0, length + 2);
			} else if (command.charAt(0) == '>') {
				if (strings.get(strings.size()-1).equals("<")){
					strings.remove(strings.size()-1);
					strings.add(">");
				} else {
					strings.add(">");
				}
				if (tmp.length() > 0) {
					setProperParam(tmp.toString());
					tmp.delete(0, tmp.length());
				}
				command.deleteCharAt(0);
				if (parsing == PARSER_OUTPUT) {
					parsing = PARSER_APPEND;
				} else {
					parsing = PARSER_OUTPUT;
				}
			} else if (command.charAt(0) == '<') {
				strings.add("<");
				if (tmp.length() > 0) {
					setProperParam(tmp.toString());
					tmp.delete(0, tmp.length());
				}
				command.deleteCharAt(0);
				parsing = PARSER_INPUT;
			} else if (command.charAt(0) == '|') {
				strings.add("|");
				if (tmp.length() > 0) {
					setProperParam(tmp.toString());
					tmp.delete(0, tmp.length());
				}
				parsing = PARSER_COMMAND;
				break;
			} else if (GrammarParser.isParameterChar(command.charAt(0))) {
				moveChar(command, tmp);
			} else if (Character.isWhitespace(command.charAt(0))) {
				break;
			} else {
				throw new OSException("Parameter contains invalid characters.");
			}
		}
		
		if (tmp.length() > 0) {
			setProperParam(tmp.toString());
		}
		
		return command;
		
	}
	
	/**
	 * Nacteni prikazu.
	 * @param command Nacitana radka.
	 * @return Zbytek nacitane radky.
	 * @throws OSException Vyjimka v pripade nacteni nepovoleneho znaku nebo prazdne radky.
	 */
	private StringBuilder readCommand(StringBuilder command) throws OSException {
		
		StringBuilder tmp = new StringBuilder();

		command = readSpace(command);
		if (command.length() == 0) {
			throw new OSException("Empty command.");
		}
		
		if (Character.isLetterOrDigit(command.charAt(0))) {
			moveChar(command, tmp);
		} else {
			throw new OSException("Command must start with [a-zA-Z0-9].");
		}

		while (command.length() > 0) {
			if (GrammarParser.isCommandChar(command.charAt(0))) {
				moveChar(command, tmp);
			} else if (GrammarParser.isParameterSeparatorChar(command.charAt(0))) {
				break;
			} else {
				throw new OSException("Command must contain [a-zA-Z0-9+-_.].");
			}
		}
		
		strings.add(tmp.toString());
		lastCommand.setName(tmp.toString());
		parsing = PARSER_PARAM;

		return command;		
	}
	
	/**
	 * Nacteni mezer.
	 * @param command Nacitana radka.
	 * @return Zbytek nacitane radky.
	 */
	private StringBuilder readSpace(StringBuilder command) {

		while (command.length() > 0) {
			if (Character.isWhitespace(command.charAt(0))) {
				command.deleteCharAt(0);
			} else {
				break;
			}
		}

		return command;
		
	}

	/**
	 * Parsovani vstupniho retezce na jednotlive prikazy a jejich parametry.
	 * @param command Vstupni retezec.
	 * @return Pole prikazu.
	 * @throws OSException Vyjimka v pripade chyby pri parsovani.
	 */
	public AbstractProcessHeader[] parseCommand(String command) throws OSException {
		
		strings = new ArrayList<String>();
		fcommands = new ArrayList<AbstractProcessHeader>();
		
		StringBuilder tmp = new StringBuilder(command);

		while (tmp.length() > 0) {
			lastCommand = new AbstractProcessHeader(null, null, "", "", false, shellID, userID);
			tmp = readCommand(tmp);
			while (tmp.length() > 0) {
				tmp = readParameter(tmp);
				if (tmp.length() > 0 && tmp.charAt(0) == '|') {
					tmp.deleteCharAt(0);
					break;
				}
			}
			fcommands.add(lastCommand);
		}
		
		/*for (AbstractProcessHeader aph: fcommands) {
			System.out.println("Command:");
			System.out.println("  name:   " + aph.getName());
			System.out.println("  input:  " + aph.getInput());
			System.out.println("  output: " + aph.getOutput() + " " + ((aph.getAppend()) ? "(append)" : ""));
			System.out.println("  params: " + aph.getParams().toString());
		}*/
				
		return fcommands.toArray(new AbstractProcessHeader[fcommands.size()]);
		
	}
	
	/**
	 * Parsovani vstupniho retezce do pole retezcu.
	 * @param command Vstupni retezec.
	 * @return Pole retezcu.
	 */
	public List<String> parseToStrings(String command) {
		
		try {
			parseCommand(command);
		} catch (OSException e) {
			System.out.println(e.getMessage());
		}
		//System.out.println(strings);
		
		return strings;
		
	}

}
